/*
 * Copyright 2003-2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package gs

import com.thoughtworks.selenium.*

/**
 * Extends the SeleneseTestCase class with a few Groovy metaprogramming tricks.
 */
abstract class GroovierSeleniumTestCase extends SeleneseTestCase {

    /**
     * Creates a new GroovierSeleniumTestCase object.
     */
    def GroovierSeleniumTestCase(String name) {
        super(name)
    }

    /**
     * Called by Groovy when a method isn't present in the class. We first try
     * to find the method against the selenium object. We then try to match the 
     * start of the method name to see whether we need to wait for a condition
     * to either be true or false.
     *
     * @param name The name of the method missing from the class.
     * @param args The arguments that were passed to the missing method.
     */
    def methodMissing(String name, args) {
        switch (name) {
            case ~/waitForNot.*/: return waitForNot(name, args)
            case ~/waitFor.*/: return waitFor(name, args)
            case ~/assertNot.*/:
                assertNot(name, args)
                break
            case ~/assert.*/:
                assertThat(name, args)
                break
            case ~/verifyNot.*/:
                return verifyNot(name, args)
            case ~/verify.*/:
                return verifyThat(name, args)
            default:
                return createAndCallMethod(name, args)
        }
    }

    /**
     * Asserts that the method called returns a falsy result
     */
    private void assertNot(String name, args) {
        def methodName = getMethodName("assertNot", name)
        assert !"$methodName"(* args)
    }

    /**
     * Assert that the method's return is truthy
     */
    private void assertThat(String name, args) {
        def methodName = getMethodName("assert", name)
        assert "$methodName"(* args)
    }

    private Object verifyThat(name, args) {
        def methodName = getMethodName("verify", name)
        return "$methodName"(* args)
    }

    private Object verifyNot(name, args) {
        def methodName = getMethodName("verifyNot", name)
        return !"$methodName"(* args)
    }

    /**
     * Waits for a page to load.
     * @param timeout The number of millis to wait for the page load to timeout.
     */
    def waitForPageToLoad(timeout = 60000) {
        selenium.waitForPageToLoad("$timeout")
    }

    /**
     * Wait for the condition provided in the script to turn true or for a timeout
     * to expire.
     * @param script The script that is repeatedly evaluated.
     * @param timeout The timeout associated with this call.
     */
    def waitForCondition(script, timeout = 60000) {
        selenium.waitForCondition(script, "$timeout")
    }

    /**
     * Wait for a frame to load up to a provided timeout.
     * @param frameAddress The URL to the frame.
     * @param timeout The time out associated with this call.
     */
    def waitForFrameToLoad(frameAddress, timeout = 60000) {
        selenium.waitForFrameToLoad(frameAddress, "$timeout")
    }

    /**
     * Wait for a popup to show up.
     * @param windowId The JavaScript id associated with the window.
     * @param timeout The timeout associated with the call.
     */
    def waitForPopUp(windowId, timeout = 60000) {
        selenium.waitForPopUp(windowId, "$timeout")
    }

    /**
     * Adds a method to the meta class and then calls the same method against the 
     * selenium object.
     * @param name The name of the method we are adding.
     * @param args The arguments passed to the method in question.
     * @return What is returned by the delegated method call.
     */
    private Object createAndCallMethod(name, args) {
        GroovierSeleniumTestCase.metaClass."$name" = {Object varArgs ->
            delegate.selenium.metaClass.invokeMethod(delegate.selenium, name, varArgs)
        }
        return selenium."$name"(* args)
    }

    /**
     * Waits for a condition to be true or a timeout occurs.
     * @param name The name of the method that was passed to methodMissing.
     * @param args The arguments that were passed to methodMissing.
     * @return Whether the condition is true or false.
     */
    private waitFor(name, args) {
        // Make the bold assumption that the time out is the first param.
        def timeout = args[0]
        if (timeout instanceof Integer) {
            args = args[1..args.length - 1].toArray()
        } else {
            timeout = 60000
        }
        def methodName = getMethodName("waitFor", name);
        for (i in 0..(timeout / 1000)) {
            if ("$methodName"(* args)) {
                return true;
            }
            sleep(1000)
        }
        fail("Timeout occured in $name for $args")
    }

    /**
     * Finds the first method that matches the selenium method used to retrieve some
     * values. 
     *
     * @param prefix The prefix that we are looking for.
     * @param name The method that was called in the test.
     * @return The name of the method that we did find.
     */
    def getMethodName(prefix, name) {
        ["is", "get"].collect {
            name.replaceFirst(prefix, it)
        }.find {
            delegate.selenium.metaClass.respondsTo(delegate.selenium, it)
        }
    }

    /**
     * Wait for a condition to be false. Will wait for a minute by default.
     * @param name The name of the method we are looking for.
     * @param args The arguments that were passed to the missing method.
     * @return Returns whether the word was found.
     */
    private waitForNot(name, args) {
        def timeout = args[0]
        if (timeout instanceof Integer) {
            args = args[1..args.length - 1].toArray()
        } else {
            timeout = 60000
        }
        def methodName = getMethodName("waitForNot", name)
        for (i in 0..(timeout / 1000)) {
            if (!"$methodName"(* args)) {
                return true
            }
        }
        fail("Timeout occured in $name for $args")
    }


    /**
     * Delegates the property resolution to the selenium object if the 
     * property cannot be found in this class.
     */
    def propertyMissing(String name) {
        selenium."$name"
    }
}
